3 namespace MediaWiki\Tests\Revision
;
7 use InvalidArgumentException
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Revision\RevisionAccessException
;
11 use MediaWiki\Revision\RevisionStore
;
12 use MediaWiki\Revision\SlotRoleRegistry
;
13 use MediaWiki\Revision\SlotRecord
;
14 use MediaWiki\Storage\SqlBlobStore
;
15 use MediaWikiTestCase
;
19 use Wikimedia\Rdbms\Database
;
20 use Wikimedia\Rdbms\LoadBalancer
;
21 use Wikimedia\TestingAccessWrapper
;
24 class RevisionStoreTest
extends MediaWikiTestCase
{
26 private function useTextId() {
27 global $wgMultiContentRevisionSchemaMigrationStage;
29 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD
);
33 * @param LoadBalancer $loadBalancer
34 * @param SqlBlobStore $blobStore
35 * @param WANObjectCache $WANObjectCache
37 * @return RevisionStore
39 private function getRevisionStore(
42 $WANObjectCache = null
44 global $wgMultiContentRevisionSchemaMigrationStage;
45 // the migration stage should be irrelevant, since all the tests that interact with
46 // the database are in RevisionStoreDbTest, not here.
48 return new RevisionStore(
49 $loadBalancer ?
: $this->getMockLoadBalancer(),
50 $blobStore ?
: $this->getMockSqlBlobStore(),
51 $WANObjectCache ?
: $this->getHashWANObjectCache(),
52 MediaWikiServices
::getInstance()->getCommentStore(),
53 MediaWikiServices
::getInstance()->getContentModelStore(),
54 MediaWikiServices
::getInstance()->getSlotRoleStore(),
55 MediaWikiServices
::getInstance()->getSlotRoleRegistry(),
56 $wgMultiContentRevisionSchemaMigrationStage,
57 MediaWikiServices
::getInstance()->getActorMigration()
62 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
64 private function getMockLoadBalancer() {
65 return $this->getMockBuilder( LoadBalancer
::class )
66 ->disableOriginalConstructor()->getMock();
70 * @return \PHPUnit_Framework_MockObject_MockObject|Database
72 private function getMockDatabase() {
73 return $this->getMockBuilder( Database
::class )
74 ->disableOriginalConstructor()->getMock();
78 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
80 private function getMockSqlBlobStore() {
81 return $this->getMockBuilder( SqlBlobStore
::class )
82 ->disableOriginalConstructor()->getMock();
86 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
88 private function getMockCommentStore() {
89 return $this->getMockBuilder( CommentStore
::class )
90 ->disableOriginalConstructor()->getMock();
94 * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
96 private function getMockSlotRoleRegistry() {
97 return $this->getMockBuilder( SlotRoleRegistry
::class )
98 ->disableOriginalConstructor()->getMock();
101 private function getHashWANObjectCache() {
102 return new WANObjectCache( [ 'cache' => new \
HashBagOStuff() ] );
105 public function provideSetContentHandlerUseDB() {
107 // ContentHandlerUseDB can be true of false pre migration.
108 [ false, SCHEMA_COMPAT_OLD
, false ],
109 [ true, SCHEMA_COMPAT_OLD
, false ],
110 // During and after migration it can not be false...
111 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, true ],
112 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, true ],
113 [ false, SCHEMA_COMPAT_NEW
, true ],
114 // ...but it can be true.
115 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
116 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
117 [ true, SCHEMA_COMPAT_NEW
, false ],
122 * @dataProvider provideSetContentHandlerUseDB
123 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
124 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
126 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
127 if ( $expectedFail ) {
128 $this->setExpectedException( MWException
::class );
131 $nameTables = MediaWikiServices
::getInstance()->getNameTableStoreFactory();
133 $store = new RevisionStore(
134 $this->getMockLoadBalancer(),
135 $this->getMockSqlBlobStore(),
136 $this->getHashWANObjectCache(),
137 $this->getMockCommentStore(),
138 $nameTables->getContentModels(),
139 $nameTables->getSlotRoles(),
140 $this->getMockSlotRoleRegistry(),
142 MediaWikiServices
::getInstance()->getActorMigration()
145 $store->setContentHandlerUseDB( $contentHandlerDb );
146 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
150 * @covers \MediaWiki\Revision\RevisionStore::getTitle
152 public function testGetTitle_successFromPageId() {
153 $mockLoadBalancer = $this->getMockLoadBalancer();
154 // Title calls wfGetDB() so we have to set the main service
155 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
157 $db = $this->getMockDatabase();
158 // Title calls wfGetDB() which uses a regular Connection
159 $mockLoadBalancer->expects( $this->atLeastOnce() )
160 ->method( 'getConnection' )
163 // First call to Title::newFromID, faking no result (db lag?)
164 $db->expects( $this->at( 0 ) )
165 ->method( 'selectRow' )
171 ->willReturn( (object)[
172 'page_namespace' => '1',
173 'page_title' => 'Food',
176 $store = $this->getRevisionStore( $mockLoadBalancer );
177 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
179 $this->assertSame( 1, $title->getNamespace() );
180 $this->assertSame( 'Food', $title->getDBkey() );
184 * @covers \MediaWiki\Revision\RevisionStore::getTitle
186 public function testGetTitle_successFromPageIdOnFallback() {
187 $mockLoadBalancer = $this->getMockLoadBalancer();
188 // Title calls wfGetDB() so we have to set the main service
189 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
191 $db = $this->getMockDatabase();
192 // Title calls wfGetDB() which uses a regular Connection
193 // Assert that the first call uses a REPLICA and the second falls back to master
194 $mockLoadBalancer->expects( $this->exactly( 2 ) )
195 ->method( 'getConnection' )
197 // RevisionStore getTitle uses a ConnectionRef
198 $mockLoadBalancer->expects( $this->atLeastOnce() )
199 ->method( 'getConnectionRef' )
202 // First call to Title::newFromID, faking no result (db lag?)
203 $db->expects( $this->at( 0 ) )
204 ->method( 'selectRow' )
210 ->willReturn( false );
212 // First select using rev_id, faking no result (db lag?)
213 $db->expects( $this->at( 1 ) )
214 ->method( 'selectRow' )
216 [ 'revision', 'page' ],
220 ->willReturn( false );
222 // Second call to Title::newFromID, no result
223 $db->expects( $this->at( 2 ) )
224 ->method( 'selectRow' )
230 ->willReturn( (object)[
231 'page_namespace' => '2',
232 'page_title' => 'Foodey',
235 $store = $this->getRevisionStore( $mockLoadBalancer );
236 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
238 $this->assertSame( 2, $title->getNamespace() );
239 $this->assertSame( 'Foodey', $title->getDBkey() );
243 * @covers \MediaWiki\Revision\RevisionStore::getTitle
245 public function testGetTitle_successFromRevId() {
246 $mockLoadBalancer = $this->getMockLoadBalancer();
247 // Title calls wfGetDB() so we have to set the main service
248 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
250 $db = $this->getMockDatabase();
251 // Title calls wfGetDB() which uses a regular Connection
252 $mockLoadBalancer->expects( $this->atLeastOnce() )
253 ->method( 'getConnection' )
255 // RevisionStore getTitle uses a ConnectionRef
256 $mockLoadBalancer->expects( $this->atLeastOnce() )
257 ->method( 'getConnectionRef' )
260 // First call to Title::newFromID, faking no result (db lag?)
261 $db->expects( $this->at( 0 ) )
262 ->method( 'selectRow' )
268 ->willReturn( false );
270 // First select using rev_id, faking no result (db lag?)
271 $db->expects( $this->at( 1 ) )
272 ->method( 'selectRow' )
274 [ 'revision', 'page' ],
278 ->willReturn( (object)[
279 'page_namespace' => '1',
280 'page_title' => 'Food2',
283 $store = $this->getRevisionStore( $mockLoadBalancer );
284 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
286 $this->assertSame( 1, $title->getNamespace() );
287 $this->assertSame( 'Food2', $title->getDBkey() );
291 * @covers \MediaWiki\Revision\RevisionStore::getTitle
293 public function testGetTitle_successFromRevIdOnFallback() {
294 $mockLoadBalancer = $this->getMockLoadBalancer();
295 // Title calls wfGetDB() so we have to set the main service
296 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
298 $db = $this->getMockDatabase();
299 // Title calls wfGetDB() which uses a regular Connection
300 // Assert that the first call uses a REPLICA and the second falls back to master
301 $mockLoadBalancer->expects( $this->exactly( 2 ) )
302 ->method( 'getConnection' )
304 // RevisionStore getTitle uses a ConnectionRef
305 $mockLoadBalancer->expects( $this->atLeastOnce() )
306 ->method( 'getConnectionRef' )
309 // First call to Title::newFromID, faking no result (db lag?)
310 $db->expects( $this->at( 0 ) )
311 ->method( 'selectRow' )
317 ->willReturn( false );
319 // First select using rev_id, faking no result (db lag?)
320 $db->expects( $this->at( 1 ) )
321 ->method( 'selectRow' )
323 [ 'revision', 'page' ],
327 ->willReturn( false );
329 // Second call to Title::newFromID, no result
330 $db->expects( $this->at( 2 ) )
331 ->method( 'selectRow' )
337 ->willReturn( false );
339 // Second select using rev_id, result
340 $db->expects( $this->at( 3 ) )
341 ->method( 'selectRow' )
343 [ 'revision', 'page' ],
347 ->willReturn( (object)[
348 'page_namespace' => '2',
349 'page_title' => 'Foodey',
352 $store = $this->getRevisionStore( $mockLoadBalancer );
353 $title = $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
355 $this->assertSame( 2, $title->getNamespace() );
356 $this->assertSame( 'Foodey', $title->getDBkey() );
360 * @covers \MediaWiki\Revision\RevisionStore::getTitle
362 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
363 $mockLoadBalancer = $this->getMockLoadBalancer();
364 // Title calls wfGetDB() so we have to set the main service
365 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
367 $db = $this->getMockDatabase();
368 // Title calls wfGetDB() which uses a regular Connection
369 // Assert that the first call uses a REPLICA and the second falls back to master
371 // RevisionStore getTitle uses getConnectionRef
372 // Title::newFromID uses getConnection
373 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
374 $mockLoadBalancer->expects( $this->exactly( 2 ) )
376 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
377 static $callCounter = 0;
379 // The first call should be to a REPLICA, and the second a MASTER.
380 if ( $callCounter === 1 ) {
381 $this->assertSame( DB_REPLICA
, $masterOrReplica );
382 } elseif ( $callCounter === 2 ) {
383 $this->assertSame( DB_MASTER
, $masterOrReplica );
388 // First and third call to Title::newFromID, faking no result
389 foreach ( [ 0, 2 ] as $counter ) {
390 $db->expects( $this->at( $counter ) )
391 ->method( 'selectRow' )
397 ->willReturn( false );
400 foreach ( [ 1, 3 ] as $counter ) {
401 $db->expects( $this->at( $counter ) )
402 ->method( 'selectRow' )
404 [ 'revision', 'page' ],
408 ->willReturn( false );
411 $store = $this->getRevisionStore( $mockLoadBalancer );
413 $this->setExpectedException( RevisionAccessException
::class );
414 $store->getTitle( 1, 2, RevisionStore
::READ_NORMAL
);
417 public function provideNewRevisionFromRow_legacyEncoding_applied() {
418 yield
'windows-1252, old_flags is empty' => [
423 'old_text' => "S\xF6me Content",
428 yield
'windows-1252, old_flags is null' => [
433 'old_text' => "S\xF6me Content",
440 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
442 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
444 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
445 if ( !$this->useTextId() ) {
446 $this->markTestSkipped( 'No longer applicable with MCR schema' );
449 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
450 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
452 $blobStore = new SqlBlobStore( $lb, $cache );
453 $blobStore->setLegacyEncoding( $encoding, Language
::factory( $locale ) );
455 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
457 $record = $store->newRevisionFromRow(
458 $this->makeRow( $row ),
460 Title
::newFromText( __METHOD__
. '-UTPage' )
463 $this->assertSame( $text, $record->getContent( SlotRecord
::MAIN
)->serialize() );
467 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
469 public function testNewRevisionFromRow_legacyEncoding_ignored() {
470 if ( !$this->useTextId() ) {
471 $this->markTestSkipped( 'No longer applicable with MCR schema' );
475 'old_flags' => 'utf-8',
476 'old_text' => 'Söme Content',
479 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
480 $lb = MediaWikiServices
::getInstance()->getDBLoadBalancer();
482 $blobStore = new SqlBlobStore( $lb, $cache );
483 $blobStore->setLegacyEncoding( 'windows-1252', Language
::factory( 'en' ) );
485 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
487 $record = $store->newRevisionFromRow(
488 $this->makeRow( $row ),
490 Title
::newFromText( __METHOD__
. '-UTPage' )
492 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord
::MAIN
)->serialize() );
495 private function makeRow( array $array ) {
499 'rev_timestamp' => '20110101000000',
500 'rev_user_text' => 'Tester',
502 'rev_minor_edit' => 0,
505 'rev_parent_id' => 0,
506 'rev_sha1' => 'deadbeef',
507 'rev_comment_text' => 'Testing',
508 'rev_comment_data' => '{}',
509 'rev_comment_cid' => 111,
510 'page_namespace' => 0,
511 'page_title' => 'TEST',
514 'page_is_redirect' => 0,
516 'user_name' => 'Tester',
519 if ( $this->useTextId() ) {
521 'rev_content_format' => CONTENT_FORMAT_TEXT
,
522 'rev_content_model' => CONTENT_MODEL_TEXT
,
525 'old_text' => 'Hello World',
526 'old_flags' => 'utf-8',
529 if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
531 'main' => new WikitextContent( $array['old_text'] ),
539 public function provideMigrationConstruction() {
541 [ SCHEMA_COMPAT_OLD
, false ],
542 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
, false ],
543 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW
, false ],
544 [ SCHEMA_COMPAT_NEW
, false ],
545 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH
, true ],
546 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH
, true ],
547 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH
, true ],
552 * @covers \MediaWiki\Revision\RevisionStore::__construct
553 * @dataProvider provideMigrationConstruction
555 public function testMigrationConstruction( $migration, $expectException ) {
556 if ( $expectException ) {
557 $this->setExpectedException( InvalidArgumentException
::class );
559 $loadBalancer = $this->getMockLoadBalancer();
560 $blobStore = $this->getMockSqlBlobStore();
561 $cache = $this->getHashWANObjectCache();
562 $commentStore = $this->getMockCommentStore();
563 $services = MediaWikiServices
::getInstance();
564 $nameTables = $services->getNameTableStoreFactory();
565 $contentModelStore = $nameTables->getContentModels();
566 $slotRoleStore = $nameTables->getSlotRoles();
567 $slotRoleRegistry = $services->getSlotRoleRegistry();
568 $store = new RevisionStore(
573 $nameTables->getContentModels(),
574 $nameTables->getSlotRoles(),
577 $services->getActorMigration()
579 if ( !$expectException ) {
580 $store = TestingAccessWrapper
::newFromObject( $store );
581 $this->assertSame( $loadBalancer, $store->loadBalancer
);
582 $this->assertSame( $blobStore, $store->blobStore
);
583 $this->assertSame( $cache, $store->cache
);
584 $this->assertSame( $commentStore, $store->commentStore
);
585 $this->assertSame( $contentModelStore, $store->contentModelStore
);
586 $this->assertSame( $slotRoleStore, $store->slotRoleStore
);
587 $this->assertSame( $migration, $store->mcrMigrationStage
);